Young Choi
Young Choi

Categories

When you build a REST API with Spring, one endpoint often has to accept several kinds of request. Using an interface for the request body lets a single endpoint handle them all. The example here is a restaurant order system, where one /v1/orders endpoint takes both hamburger and pizza orders.

Scenario: Restaurant Order System

We have an endpoint /v1/orders that accepts different order types. Every order shares a few fields and adds its own. We define an Order interface and implement it with one class per order type.

Step 1: Define the Order Interface

Start with an Order interface holding what every order has in common.

public interface Order {
    String getOrderType();
}

Step 2: Implement Order Types

Then write a class per order type, like HamburgerOrder and PizzaOrder.

import lombok.Data;
import javax.validation.constraints.NotNull;

@Data
public class HamburgerOrder implements Order {
    @NotNull
    private String orderType = "hamburger";
    @NotNull
    private String bunType;
    @NotNull
    private String meatType;
    private boolean extraCheese;
}
import lombok.Data;
import javax.validation.constraints.NotNull;

@Data
public class PizzaOrder implements Order {
    @NotNull
    private String orderType = "pizza";
    @NotNull
    private String crustType;
    @NotNull
    private String size;
    private boolean extraToppings;
}

Step 3: Create the Controller

The controller handles the /v1/orders POST request. The @RequestBody and @Valid annotations bind and validate the incoming JSON.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
@RequestMapping("/v1/orders")
@Validated
public class OrderController {

    @PostMapping
    public ResponseEntity<String> createOrder(@Valid @RequestBody Order order) {
        if (order instanceof HamburgerOrder) {
            HamburgerOrder hamburgerOrder = (HamburgerOrder) order;
            // Process hamburger order
            return new ResponseEntity<>("Hamburger order received", HttpStatus.OK);
        } else if (order instanceof PizzaOrder) {
            PizzaOrder pizzaOrder = (PizzaOrder) order;
            // Process pizza order
            return new ResponseEntity<>("Pizza order received", HttpStatus.OK);
        } else {
            return new ResponseEntity<>("Unknown order type", HttpStatus.BAD_REQUEST);
        }
    }
}

Step 4: Configure Jackson for Polymorphic Deserialization

Jackson needs to know which class to build from the JSON. Annotate the Order interface so it reads the orderType field and picks the matching implementation.

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "orderType"
)
@JsonSubTypes({
        @JsonSubTypes.Type(value = HamburgerOrder.class, name = "hamburger"),
        @JsonSubTypes.Type(value = PizzaOrder.class, name = "pizza")
})
public interface Order {
    String getOrderType();
}

Step 5: Add Global Validation Handling

Finally, handle validation errors in one place.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<String> handleValidationExceptions(MethodArgumentNotValidException ex) {
        return new ResponseEntity<>("Invalid order data", HttpStatus.BAD_REQUEST);
    }
}

Conclusion

An interface for the request body lets one endpoint accept many order types without a pile of branching at the door. We defined an Order interface, implemented it per type, let Jackson pick the right class from orderType, and validated with @Valid. Adding a new order type now means writing one class and one Jackson subtype, and nothing else has to change. Happy coding!